import { isString } from 'lodash-es'
import path from 'path'
import { AcornCodeKey, AcornNodePri, CompluginContext, CompluginTransformContext, SourceMap, TransformResult } from '../../types'
import { acornParse, CWD, generateCodeFrameMessage, isAst } from '../../utils'
import { CompluginWebpackPluginContext } from '../injected'
import { doResolve, emitAsset, emitChunk, emitStatic, PLUGIN_KEY, WebpackLoaderContext } from '../utils'

const _emitChunkWithLoader = (
  request: string,
  loader: WebpackLoaderContext,
  options?: import('webpack').EntryOptions
) => {
  return emitChunk(request, loader._compilation!, options)
}

export const emitChunkWithLoader = (request: string, loader: WebpackLoaderContext) => {
  return _emitChunkWithLoader(request, loader)
}

export const emitIifeWithLoader = (request: string, loader: WebpackLoaderContext) => {
  return _emitChunkWithLoader(request, loader, {
    chunkLoading: false,
    library: { type: 'var' }
  })
}

export const getCompluginNameFromLoader = (loader: WebpackLoaderContext) => {
  const { query } = loader
  const compluginName =
    query && typeof query === 'object' ? (query as any)[PLUGIN_KEY] : (loader.getOptions?.() as any)?.[PLUGIN_KEY]

  return compluginName
}

export const getPluginContextFromLoader = (loader: WebpackLoaderContext) => {
  const compluginName = getCompluginNameFromLoader(loader)
  if (typeof compluginName === 'string') {
    const pluginContext = loader._compiler?.$compluginInjected?.plugins[compluginName]
    if (pluginContext) {
      return pluginContext
    }
  }
}

export const getTransformContext = (
  loader: WebpackLoaderContext,
  inputCode: string,
  id: string,
  resource: string,
  pluginContext: CompluginWebpackPluginContext
): CompluginTransformContext => {
  const { transformLogger, compluginName } = pluginContext
  return {
    logger: transformLogger,
    sourceMap: !!loader.sourceMap,
    parse: acornParse(),
    emitChunk: id => emitChunkWithLoader(id, loader),
    emitIife: id => emitIifeWithLoader(id, loader),
    emitAsset: asset => emitAsset(asset, loader._compilation!),
    emitFile: asset => void emitAsset(asset, loader._compilation!),
    emitStatic: fileName => emitStatic(fileName, loader._compilation!),
    resolve: (source, importer) => doResolve(source, importer, loader._compilation!),
    addWatchFile: fileName => void loader.addDependency(path.resolve(CWD, fileName)),
    rebuild: () => {
      const injected = loader._compiler?.$compluginInjected
      const compilation = loader._compilation

      if (injected && compilation) {
        injected.rebuild(resource, compilation)
      }
    },
    error: (message, pos) => {
      loader.emitError(
        new Error(`[${compluginName}:webpack/transform]: ${generateCodeFrameMessage(message, id, inputCode, pos as any)}`)
      )
    },
    warn: (message, pos) => {
      loader.emitWarning(
        new Error(`[${compluginName}:webpack/transform]: ${generateCodeFrameMessage(message, id, inputCode, pos as any)}`)
      )
    },
    // @ts-ignore
    getWatchFiles: loader.getDependencies.bind(loader)
  }
}

const HTMLWebpackPluginLoader = path.normalize('node_modules/html-webpack-plugin')

const excludeLoader = (loader: string) => {
  if (loader.includes(HTMLWebpackPluginLoader)) return true
  return false
}

export const excludeLoadersOnPitch = (_this: WebpackLoaderContext, loaderFn: Function) => {
  const loaders = _this.loaders
  if (loaders.some(loader => excludeLoader(loader.path))) {
    const filteredLoaders = loaders.filter(loader => loader.normal !== loaderFn)
    if (filteredLoaders.length !== loaders.length) {
      loaders.splice(0, loaders.length, ...filteredLoaders)
    }
  }
}

export const handleTransformResult = (
  originalCode: string,
  originalSourcemap: SourceMap | string | null | undefined,
  result: TransformResult,
  _this: WebpackLoaderContext
) => {
  if (result == null) {
    _this.callback(null, originalCode, originalSourcemap!)
  } else if (isString(result)) {
    _this.callback(null, String(result))
  } else if ((result as any).code != null) {
    let { code, map, ast } = result

    code = String(code)

    if (isAst(ast)) {
      ;(ast as AcornNodePri)[AcornCodeKey] = code
    } else {
      ast = undefined
    }

    _this.callback(null, code, map!, ast ? { webpackAST: ast } : undefined)
  }
}
